﻿using System;
using System.Collections.Generic;
using System.Linq;
using ApartmentBillsManager.Start.Common;

namespace ApartmentBillsManager.Start.Services
{
    static class BillsCalculator
    {
        public static List<BalanceRow> CalculateBalanceByAllBills(Apartment apartment)
        {
            var newBalance =
                apartment.Roommates.Select(roommate => new BalanceRow { RoommateName = roommate.Name, Balance = 0 }).ToList();

            foreach (Bill bill in apartment.OpenBills)
            {
                bill.Phases = GenerateApartmentPhasesInBill(apartment.Roommates.ToList(), bill);

                foreach (var phase in bill.Phases)
                {
                    //if (phase.PartialAmount.Equals(0) || phase.RelevantRoommates.Count == 0) continue;

                    var share = Math.Round(phase.PartialAmount / phase.RelevantRoommates.Count(), 2);

                    foreach (var roommate in phase.RelevantRoommates)
                    {
                        if (roommate.Name != bill.Paid.Name)
                        {
                            newBalance.First(row => row.RoommateName == roommate.Name).Balance += share;
                        }
                    }

                    decimal payerShare;

                    if (phase.RelevantRoommates.Any(x => x.Name == bill.Paid.Name))
                        payerShare = share * (phase.RelevantRoommates.Count - 1) * (-1);
                    else
                        payerShare = share * (phase.RelevantRoommates.Count) * (-1);

                    newBalance.First(row => row.RoommateName == bill.Paid.Name).Balance += payerShare;
                }
            }

            return newBalance;
        }

        private static List<BillPhase> GenerateApartmentPhasesInBill(List<Roommate> roommates, Bill bill)
        {
            var phases = new List<BillPhase>();

            Tuple<DateTime, DateTime> phaseRange;

            if (IsBillRelevant(roommates, bill, out phaseRange))
            {
                BillPhase currentPhase;

                var interestingDates =
                    new HashSet<InterestingDate>(
                        (roommates.Where(
                            x => x.DateOfEntrance.Date > phaseRange.Item1 && x.DateOfEntrance <= phaseRange.Item2))
                            .Select(x => new InterestingDate(x.DateOfEntrance, DateType.EntranceDate)));

                interestingDates.UnionWith(
                    roommates.Where(
                        x => x.DateOfLeaving.Date < phaseRange.Item2 && x.DateOfLeaving >= phaseRange.Item1)
                        .Select(x => new InterestingDate(x.DateOfLeaving, DateType.LeavingDate)));

                //if there are no interesting dates, the bill is being divided equally between all of the roommates
                if (interestingDates.Count == 0)
                {
                    currentPhase = new BillPhase(phaseRange.Item1, phaseRange.Item2, roommates, bill);

                    phases.Add(currentPhase);
                }
                else //if there are interesting dates
                {
                    var tempStartDate = phaseRange.Item1;

                    while (tempStartDate < phaseRange.Item2)
                    {
                        DateTime startDateOfCurrentPhase = tempStartDate;

                        InterestingDate interestingDate;

                        //gets the minimal date(s)
                        IEnumerable<InterestingDate> interestingDateOrDates =
                            interestingDates.Where(x => x.Date.Date == interestingDates.Min(y => y.Date).Date).ToList();

                        if (interestingDateOrDates.Count() == 1)
                        {
                            interestingDate = interestingDateOrDates.First();

                            currentPhase = CreateNewPhase(roommates, bill, startDateOfCurrentPhase, interestingDate);

                            phases.Add(currentPhase);
                        }
                        else if (interestingDateOrDates.Count() > 1)
                        {
                            interestingDate = interestingDateOrDates.First(x => x.DateType == DateType.LeavingDate);

                            currentPhase = CreateNewPhase(roommates, bill, startDateOfCurrentPhase, interestingDate);

                            phases.Add(currentPhase);


                            interestingDate = interestingDateOrDates.First(x => x.DateType == DateType.EntranceDate);

                            currentPhase = CreateNewPhase(roommates, bill, startDateOfCurrentPhase, interestingDate);

                            phases.Add(currentPhase);
                        }
                        else
                        {
                            phases.Add(new BillPhase(tempStartDate, phaseRange.Item2, roommates, bill));
                            break;
                        }


                        tempStartDate = currentPhase.EndDate.AddDays(1);

                        interestingDates.RemoveWhere(x => x.Date == interestingDate.Date);
                    }
                }
            }

            return phases;
        }

        private static bool IsBillRelevant(List<Roommate> roommates, Bill bill, out Tuple<DateTime, DateTime> phaseRange)
        {
            var minEntranceDate = roommates.Min(x => x.DateOfEntrance).Date;
            var maxLeavingDate = roommates.Max(x => x.DateOfLeaving).Date;

            if (minEntranceDate <= bill.StartDate.Date && !(maxLeavingDate < bill.StartDate.Date))
            {
                if (maxLeavingDate >= bill.EndDate.Date)
                {
                    phaseRange = new Tuple<DateTime, DateTime>(bill.StartDate.Date, bill.EndDate.Date);
                    return true;
                }

                phaseRange = new Tuple<DateTime, DateTime>(bill.StartDate.Date, maxLeavingDate);
                return true;
            }

            if (minEntranceDate > bill.StartDate.Date && !(minEntranceDate > bill.EndDate.Date))
            {
                if (maxLeavingDate >= bill.EndDate.Date)
                {
                    phaseRange = new Tuple<DateTime, DateTime>(minEntranceDate, bill.EndDate.Date);
                    return true;
                }

                phaseRange = new Tuple<DateTime, DateTime>(minEntranceDate, maxLeavingDate);
                return true;
            }

            phaseRange = null;
            return false;
        }


        private static BillPhase CreateNewPhase(IEnumerable<Roommate> roommates, Bill bill, DateTime startDate, InterestingDate interestingDate)
        {
            var tempDate = interestingDate.Date;

            DateTime endDate;

            switch (interestingDate.DateType)
            {
                case DateType.EntranceDate:
                    endDate = tempDate.Date.AddDays(-1);
                    break;
                case DateType.LeavingDate:
                    endDate = tempDate.Date;
                    break;
                default:
                    throw new NotImplementedException(); //find a better exception
            }

            var relevantRoommates =
                            roommates.Where(
                                x =>
                                x.DateOfEntrance.Date <= startDate.Date &&
                                x.DateOfLeaving.Date >= endDate.Date).ToList();

            return new BillPhase(startDate, endDate, relevantRoommates, bill);
        }

        public static List<DebtPresenter> GenerateDebts(List<BalanceRow> balance)
        {
            var debts = new List<DebtPresenter>();

            var payers = new List<BalanceRow>(balance.Where(row => row.Balance > 0));
            var payees = new List<BalanceRow>(balance.Where(row => row.Balance < 0));

            foreach (BalanceRow payer in payers)
            {
                var tempDebt = new DebtPresenter()
                {
                    From = payer.RoommateName,
                    Date = DateTime.Now
                };

                foreach (BalanceRow payee in payees)
                {
                    if (payee.Balance < 0)
                    {
                        tempDebt.To = payee.RoommateName;

                        if (payer.Balance + payee.Balance >= 0)
                        {
                            tempDebt.Amount = payee.Balance * -1;
                        }
                        else
                        {
                            tempDebt.Amount = payer.Balance;
                            break;
                        }

                    }
                }

                if (tempDebt.To != null)
                    debts.Add(tempDebt);
            }

            return debts;
        }

    }

    class InterestingDate
    {
        public DateTime Date;
        public DateType DateType;

        public InterestingDate(DateTime date, DateType dateType)
        {
            Date = date;
            DateType = dateType;
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != this.GetType()) return false;
            return Equals((InterestingDate)obj);
        }

        protected bool Equals(InterestingDate other)
        {
            return Date.Equals(other.Date) && DateType == other.DateType;
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (Date.GetHashCode() * 397) ^ (int)DateType;
            }
        }
    }

    enum DateType
    {
        EntranceDate,
        LeavingDate
    }
}
